Quais as associações entre as cartas de Gwent nos decks existentes?

Eu tenho jogado Gwent: the Witcher Card Game há algum tempo, e é impressionante a quantidade de combos e sinergias que podem haver entre as cartas de acordo com o deck que você monta. Neste post, eu tento identificar as combinações de cartas que aparecem com maior frequência através de uma análise das regras de associação entre elas.

boardgames
arules
analise
r
Autor
Data de Publicação

8 de janeiro de 2022

Data de Modificação

10 de fevereiro de 2024

Motivação

Gwent é um jogo de cartas do universo de The Witcher no qual dois jogadores se enfrentam em busca da maior pontuação em pelo menos 2 de 3 rodadas, cada uma com no máximo uns 10 turnos para cada jogador. Esta pontuação é dada pelo poder de cada carta e, também, através da forma que elas interagem entre si. A tabela abaixo traz um exemplo disto para 4 cartas pertencentes aos decks da facção Scoia’tael - uma das 6 facções existentes no jogo.

Código
# carrega os pacotes
library(tidyverse) # core
library(reactable) # para tabelas interativas
library(reactablefmtr) # para embedar imagens no reactable

# carrega o exemplo
read_rds(file = 'data/decks.rds') %>%
  # pegando quatro cartas de exemplo
  filter(localizedName %in% c('Malena', 'Brigada Vrihedd', 
                              'Dol Blathanna: Guarda' , 'Bruxo Gato')) %>% 
  # extraindo um exemplo único de cada carta
  distinct(small, localizedName, power, texto) %>% 
  # juntando o prefixo do link da imagem
  mutate(small = paste0('https://www.playgwent.com/', small)) %>% 
  # organiza as cartas em ordem alfabetica
  arrange(localizedName) %>% 
  # colocando os exemplos em um reactable
  reactable(
    compact = TRUE, borderless = TRUE, defaultColDef = colDef(align = 'left'), 
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      small         = colDef(name = '', cell = embed_img(height = 80, width = 60), maxWidth = 80),
      localizedName = colDef(name = 'Carta', maxWidth = 140),
      power         = colDef(name = 'Poder', maxWidth = 50),
      texto         = colDef(name = 'Descrição')
    )
  )

Se utilizarmos um exemplar de cada uma destas cartas em uma rodada, temos um poder total de 16 (i.e., cada uma das 4 cartas tem 4 de poder). Todavia, a cada turno dentro desta rodada, podemos tomar vantagem de cada uma das cartas:

  • Usar a carta Malena para movimentar a Brigada Vrihedd para outra parte do tabuleiro, causando 2 de dano ao oponente (i.e., removendo 2 pontos do oponente);
  • Ao movimentar a carta acima, acionamos a habilidade da carta Dol Blathanna: Guarda, fazendo com que a Brigada Vrihedd ganhe mais 1 de poder;
  • Quando o nosso turno acaba, ativamos a habilidade da carta Bruxo Gato, causando mais 1 ponto de dano ao oponente (ou 2 pontos de dano, se a rodada estiver próxima do fim);
  • Finalmente, como a carta do Bruxo Gato se movimentou no tabuleiro, acionamos novamente a habilidade da carta Dol Blathanna: Guarda, adicionando 1 ponto de poder à ela.

O combo destas 4 cartas é capaz criar uma diferença de 5 pontos entre nós e o oponente à cada turno em uma rodada (i.e., removendo um total de 3 pontos de poder dele e adicionando 2 pontos de poder à nós mesmos). Se você contar os 10 turnos da rodada, e o fato de que precisamos de 4 turnos para baixar estas cartas, este combo deve girar por uns 6 turnos - nos dando uma vantagem de 30 pontos ao final dele, caso o oponente não tome nenhuma contra-medida. Além disso, ainda existem os pontos que vamos abrindo de vantagem enquanto baixamos o combo, e que dependem muito mais da estratégia de jogo (i.e., que cartas baixar em que turnos) do que da estratégia de montagem do deck em si (i.e., que cartas incluir em um deck). De toda forma, sem as cartas certas no deck não há estratégia de jogo que segure…portanto, acredito que é muito importante saber identificar estas cartas e combiná-las da melhor forma possível.

Uma forma de identificarmos estas combinações é olhar os próprios decks existentes e tentar mapear os padrões de co-ocorrência das cartas. Neste contexto, se duas ou mais cartas tendem a aparecer juntas com grande frequência entre os decks é porquê, possivelmente, esta combinação pode estar envolvida em um combo. Assim, se conseguirmos identificar os padrões de co-ocorrência entre as cartas, poderíamos usar esta informação para desenhar decks que cujas cartas tenham maior sinergia entre si - garantindo que as contra-medidas não anulem totalmente a nossa estratégia.

Com isto em mente, vou utilizar o scrapper da biblioteca de decks de Gwent que desenvolvi em outro post para analisar os padrões de co-ocorrência entre cartas de Gwent. Meu intuito aqui vai ser preparar a base de dados que já havíamos obtido anteriormente, fazer uma breve análise exploratória dos padrões existentes e, então, utilizar um algoritmo para minear regras de associações para: (1) identificar os conjuntos de cartas que ocorrem com uma frequência maior do que àquela esperada ao acaso, e (2) determinar quão diferentes são as regras de associação detectadas. Eu não espero que ter acesso à essas informações vá me dar uma vantagem competitiva frente aos outros jogadores, até porquê talento para estratégia de jogo eu não tenho, mas acredito que elas possam acabar me ajudando a montar decks mais efetivos do que aqueles horríveis que monto atualmente.

Antes de começar a olhar os dados e tudo o mais, vou fazer um breve resumo sobre as peculiaridades e regras-padrão que devem ser seguidas para montagem de um deck de um Gwent. Isto vai facilitar bastante o entendimento dos dados que teremos à nossa disposição, bem como o contexto da análise exploratória dos dados.

Um resumo sobre a montagem dos decks

  • Cada jogador pode montar quantos decks quiser, sendo que cada deck deve pertencer à uma de 6 facções: Monsters, Nilfgaard, Northern Realms, Scoia'tael, Skellige ou Syndicate;
  • As cartas de um deck devem pertencer à facção do próprio deck ou ser uma carta neutra. Existe uma única exceção: algumas cartas específicas da facção Syndicate podem ser usadas junto dos decks de outras facções (mas essas são a minoria das cartas do Syndicate);
  • Um deck é composto por, no mínimo, 25 cartas: uma carta de habilidade do líder, uma carta de estratégia e o restante das cartas de escolha livre de cada jogador;
  • Cada facção têm 7 ‘cartas’ de habilidades do líder distintas, muito relacionadas à mecânica de jogo da facção;
  • A carta de estratégia serve para dar algum tipo de vantagem à pessoa jogadora que vai iniciar a primeira rodada da partida, não estando disponível nas rodadas subsequentes, nem para a pessoa jogadora que é a segunda à jogar;
  • As cartas de estratégia são essencialmente neutras, mas existe uma versão específica delas para cada facção. Toda pessoa jogadora tem disponível desde o início do jogo a carta de estratégia neutra Vantagem Tática;
  • Existem três tipos principais de cartas de escolha livre: cartas de unidade (com valor de poder, usadas no campo de batalha), cartas especiais (de efeito imediato, que são enviadas para a pilha de descarte após serem usadas) e cartas de artefato (sem valor de poder, usadas no campo de batalha). Um deck deve ter, no mínimo, 13 cartas de unidade;
  • Uma das formas de se obter uma carta no jogo é através de sua criação pagando o custo em restos (uma das moedas do jogo). Este custo de criação está diretamente relacionadao ao grupo da carta (bronze ou ouro) e seu nível de raridade: cartas de bronze comuns custam 30 restos, enquanto as de bronze raras custam 80 restos; já as cartas de ouro épicas custam 200 restos, ao passo que as de ouro lendárias custam 800 restos;
  • O níveis de raridade de cada carta estão bastante relacionados ao quão boa ela é (com exceções, claro);
  • Podem haver até 2 cópias de cada carta de bronze em um deck, mas apenas uma cópia de cada carta de ouro;
  • Cada carta também tem um custo de provisão, que descreve o quanto ela ‘pesa’ dentro de um deck. O custo de provisão total de cada deck não deve ultrapassar 168 pontos;
  • Quanto maior o nível de raridade de uma carta, maior o seu custo de provisão.

Preparação dos dados

O scrapper da biblioteca de decks de Gwent nos fornece a lista de decks contribuídos pela comunidade no site oficial do jogo, bem como a composição de cartas associadas à cada um deles. Como existem atualizações relativamente frequentes do jogo, algumas cartas sofrem nerfs e outras podem acabar recebendo um boost, o que faz com que decks que tenham sido editados há muito tempo possam estar meio defasados com relação à sua performance. Além disso, os decks podem receber likes e dislikes da comunidade, o que funciona como um termômetro do quão bom aquele deck deve ser. Assim, resolvi focar apenas nos decks que tivessem sido editados no ano de 2021 e que receberam pelo menos um voto. Com isso, é mais provável que vamos usar decks que estejam mais atualizados e validados de alguma forma pela comunidade. O pedaço de código abaixo carrega a lista de decks disponíveis no site oficial do jogo quando escrevi este post, e os apresenta em uma tabela.

Código
# carregando os pacotes
library(tidytext) # para ajudar a trabalhar com texto
library(ggridges) # para os ridge plots
library(plotly) # para visualizacao com interatividade
library(igraph) # para plotar grafos
library(fs) # para manipular os paths

# carregando os metadados de cada deck
metadados <- read_rds(file = 'data/lista_de_decks.rds')

# fazendo alguns ajustes à base dos metadados
metadados <- metadados %>% 
  # selecionando apenas as colunas desejadas
  select(deck = id, name, faccao = slug, ano = modified, language, votes, craftingCost) %>% 
  # implementando pequenos ajustes aos dados
  mutate(
    # ajustando a coluna de data
    ano = lubridate::as_datetime(x = ano),
    ano = lubridate::year(x = ano),
    # ajustando coluna de slug
    faccao = str_to_title(string = faccao),
    faccao = case_when(faccao == 'Northernrealms' ~ 'Northern Realms',
                       faccao == 'Scoiatael' ~ "Scoia'tael",
                       TRUE ~ faccao)
  )

# printando a tabela como um reactable
metadados %>% 
  reactable(
    sortable = TRUE, filterable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
    defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      deck         = colDef(name = 'Deck'),
      name         = colDef(name = 'Nome'),
      ano          = colDef(name = 'Ano de Edição'),
      faccao       = colDef(name = 'Facção'),
      language     = colDef(name = 'Origem'),
      votes        = colDef(name = 'Votos'),
      craftingCost = colDef(name = 'Custo de Criação')
    )
  )

A segunda parte do scrapper usa o código identificador único de cada deck (i.e., coluna Deck na tabela acima) para obter a sua composição de cartas e os metadados das mesmas. Eu raspei todos os decks atenderam aos dois requisitos descritos acima, compilei os resultados para um único dataframe e fiz alguns pequenos ajustes à base, só para fins de entendimento e clareza mesmo. O código abaixo carrega a base de dados com as cartas encontradas em cada deck, e cria uma tabela para visualizarmos todas as informações existentes ao nível do deck, apenas para fins de entendimento da estrutura de dados1.

Código
# carregando os dados dos decks
decks <- read_rds(file = 'data/decks.rds')

# fazendo alguns ajustes aos dados dos decks
decks <- decks %>% 
  # removendo algumas informacoes que nao precisamos
  select(-small, -big, -fluff, -ownable, -short, -categoryName, -primaryCategoryId, -name) %>% 
  # ajustando as colunas
  mutate(
    # passando o id do deck para um inteiro, para bater com os metadados
    deck = as.integer(deck),
    # ajustando coluna do slug
    slug = str_to_title(string = slug),
    slug = case_when(slug == 'Northernrealms' ~ 'Northern Realms',
                     slug == 'Scoiatael' ~ "Scoia'tael",
                     TRUE ~ slug),
    # ajustando coluna do repeat count - quantidade daquela carta no deck
    repeatCount = repeatCount + 1,
    # contando quantidade de habilidade de cada carta
    habilidades = case_when(is.na(keywords) ~ 0,
                            TRUE ~ str_count(string = keywords, pattern = ';') + 1)
  ) %>% 
  # passando os outros strings para maiusculo
  mutate(across(.cols = c(rarity, cardGroup, type), .fns = ~ str_to_title(string = .x))) %>% 
  # juntando com id da faccao
  left_join(y = select(metadados, deck, faccao), by = 'deck')

# printando a tabela
decks %>% 
  # selecionando apenas as colunas cujas informações estejam no nível do deck
  select(deck, faccao, card_in_seq, localizedName, repeatCount, slug, type) %>% 
  # passando para o reactable
  reactable(
    sortable = TRUE, filterable = TRUE, compact = TRUE,
    highlight = TRUE, borderless = TRUE, showPageSizeOptions = TRUE,
    defaultColDef = colDef(align = 'center'), defaultPageSize = 5,
    style = list(fontFamily = "Fira Sans", fontSize = "12px"),
    columns = list(
      deck          = colDef(name = 'Deck'),
      faccao        = colDef(name = 'Facção'),
      card_in_seq   = colDef(name = 'Sequência'),
      localizedName = colDef(name = 'Carta'),
      repeatCount   = colDef(name = 'Unidades'),
      slug          = colDef(name = 'Facção da Carta'),
      type          = colDef(name = 'Tipo')
    )
  )
De volta ao topo

Reuso

Citação

BibTeX
@online{marino2022,
  author = {Marino, Nicholas},
  title = {Quais as associações entre as cartas de Gwent nos decks
    existentes?},
  date = {2022-01-08},
  url = {https://nacmarino.netlify.app//posts/2022-01-08_associacoes-gwent},
  langid = {pt}
}
Por favor, cite este trabalho como:
Marino, Nicholas. 2022. “Quais as associações entre as cartas de Gwent nos decks existentes?” January 8, 2022. https://nacmarino.netlify.app//posts/2022-01-08_associacoes-gwent.